Shows what the spatial renderer is doing: where every source is as heard by the listener, which sources share a voice, and how to tune clustering.
source.state: distance, angle and attenuation of a source.renderer.getClusters(): which sources share a voice.renderer.getStats(): voices, clusters and virtual sources at a glance.source.voice: the live filter, panning and reverb values.SpatialClusteringOptions at runtime.renderer.update();
const state = source.state;
if (state) {
console.log({
distance: state.distance.toFixed(1),
azimuth: (state.azimuth * 180 / Math.PI).toFixed(0) + " deg", // 0 = in front, 90 = right
elevation: (state.elevation * 180 / Math.PI).toFixed(0) + " deg", // 3D only
attenuation: state.attenuation.toFixed(3), // 0..1
audible: source.audible,
voice: source.voice ? (source.voice.isCluster ? `cluster of ${source.voice.size}` : "own") : "none (silent)",
cutoff: source.voice?.filter.frequency.value.toFixed(0) + " Hz",
reverbSend: source.voice?.reverbSend.gain.value.toFixed(2)
});
}
function hsl(id: string): string {
let hash = 0;
for (const char of id) hash = (hash * 31 + char.charCodeAt(0)) | 0;
return `hsl(${Math.abs(hash) % 360}, 80%, 60%)`;
}
function drawAudioDebug(ctx: CanvasRenderingContext2D) {
const { position } = renderer.listener;
// Split (solid) and merge (dashed) distance around the listener.
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
ctx.beginPath();
ctx.arc(position.x, position.y, renderer.clustering.splitDistance, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([6, 6]);
ctx.beginPath();
ctx.arc(position.x, position.y, renderer.clustering.mergeDistance, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// Sources, colored by voice. Grey = silent, red = own voice, any other color = a cluster.
for (const source of renderer.sources) {
const voice = source.voice;
ctx.fillStyle = !voice ? "#555" : voice.isCluster ? hsl(voice.id) : "#f44";
ctx.beginPath();
ctx.arc(source.position.x, source.position.y, 8, 0, Math.PI * 2);
ctx.fill();
}
// Summary.
const clusters = renderer.getClusters();
ctx.fillStyle = "#fff";
ctx.fillText(`voices: ${renderer.voices.length}, clusters: ${clusters.filter(c => c.isCluster).length}, sources: ${renderer.sources.length}`, 10, 20);
}
const stats = renderer.getStats();
console.log(`voices ${stats.voices}/${renderer.options.maxVoices}, virtual ${stats.virtual}`);
// A source that is audible but not rendered, because louder sources use the voice budget:
if (source.isVirtual) console.log(source.label, "is virtual");
If many sources are virtual and you hear sounds missing, raise renderer.options.maxVoices (if the target devices can handle it), or cluster more aggressively so far away sources share voices.
All clustering options can be changed at runtime:
// Turn clustering off to hear the difference.
renderer.clustering.enabled = false;
// Cluster more aggressively: wider angle, earlier merge.
renderer.clustering.maxAngle = Math.PI / 8; // 22.5 degrees
renderer.clustering.splitDistance = 150;
renderer.clustering.mergeDistance = 200;
// Limit how many sources can share one voice.
renderer.clustering.maxMembers = 8;
| Symptom | Try |
|---|---|
| Sources switch between cluster and own voice too often | Increase the gap between splitDistance and mergeDistance |
| A cluster sounds like it comes from the wrong direction | Lower maxAngle |
| Near and far sources end up in one cluster | Lower maxDistanceRatio |
| Too many voices (CPU usage, especially with HRTF) | Lower maxVoices, raise maxAngle, lower mergeDistance |
| Audio glitches with many sources | Lower maxVoices; in 3D consider panningModel: "equalpower" |
| Distant sounds are too dull | Raise lowpassMinFrequency in renderer.options |
| Distant sounds are too wet | Lower reverbMaxSend in renderer.options |
renderer.options (lowpass, reverb, attenuation, smoothing) can also be changed at runtime, and applies on the next update().